#!/usr/bin/perl -T
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

use 5.14.0;
use strict;
use warnings;

use lib qw(. lib local/lib/perl5);

use Bugzilla;
use Bugzilla::Bug;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Hook;
use Bugzilla::Util;
use Bugzilla::Status;
use Bugzilla::Token;

###########################################################################
# General subs
###########################################################################

sub get_string {
  my ($san_tag, $vars) = @_;
  $vars->{'san_tag'} = $san_tag;
  return get_text('sanitycheck', $vars);
}

sub Status {
  my ($san_tag, $vars, $alert) = @_;
  my $cgi = Bugzilla->cgi;
  return
    if (!$alert
    && Bugzilla->usage_mode == USAGE_MODE_CMDLINE
    && !$cgi->param('verbose'));

  if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
    my $output = $cgi->param('output') || '';
    my $linebreak = $alert ? "\nALERT: " : "\n";
    $cgi->param('error_found', 1) if $alert;
    $cgi->param('output', $output . $linebreak . get_string($san_tag, $vars));
  }
  else {
    my $start_tag = $alert ? '<p class="alert">' : '<p>';
    say $start_tag . get_string($san_tag, $vars) . "</p>";
  }
}

###########################################################################
# Start
###########################################################################

my $user = Bugzilla->login(LOGIN_REQUIRED);

my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;

# If the result of the sanity check is sent per email, then we have to
# take the user prefs into account rather than querying the web browser.
my $template;
if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
  $template = Bugzilla->template_inner($user->setting('lang'));
}
else {
  $template = Bugzilla->template;

  # Only check the token if we are running this script from the
  # web browser and a parameter is passed to the script.
  # XXX - Maybe these two parameters should be deleted once logged in?
  $cgi->delete('GoAheadAndLogIn', 'Bugzilla_restrictlogin');
  if (scalar $cgi->multi_param()) {
    my $token = $cgi->param('token');
    check_hash_token($token, ['sanitycheck']);
  }
}
my $vars            = {};
my $clear_memcached = 0;

print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_CMDLINE;

# Make sure the user is authorized to access sanitycheck.cgi.
# As this script can now alter the group_control_map table, we no longer
# let users with editbugs privs run it anymore.
$user->in_group("editcomponents")
  || ThrowUserError("auth_failure",
  {group => "editcomponents", action => "run", object => "sanity_check"});

unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
  $template->process('admin/sanitycheck/list.html.tmpl', $vars)
    || ThrowTemplateError($template->error());
}

###########################################################################
# Create missing group_control_map entries
###########################################################################

if ($cgi->param('createmissinggroupcontrolmapentries')) {
  Status('group_control_map_entries_creation');

  my $na        = CONTROLMAPNA;
  my $shown     = CONTROLMAPSHOWN;
  my $insertsth = $dbh->prepare(
    qq{INSERT INTO group_control_map
                       (group_id, product_id, membercontrol, othercontrol)
                VALUES (?, ?, $shown, $na)}
  );

  my $updatesth = $dbh->prepare(
    qq{UPDATE group_control_map
                                        SET membercontrol = $shown
                                      WHERE group_id   = ?
                                        AND product_id = ?}
  );
  my $counter = 0;

  # Find all group/product combinations used for bugs but not set up
  # correctly in group_control_map
  my $invalid_combinations = $dbh->selectall_arrayref(
    qq{    SELECT bugs.product_id,
                      bgm.group_id,
                      gcm.membercontrol,
                      groups.name,
                      products.name
                 FROM bugs
           INNER JOIN bug_group_map AS bgm
                   ON bugs.bug_id = bgm.bug_id
           INNER JOIN groups
                   ON bgm.group_id = groups.id
           INNER JOIN products
                   ON bugs.product_id = products.id
            LEFT JOIN group_control_map AS gcm
                   ON bugs.product_id = gcm.product_id
                  AND    bgm.group_id = gcm.group_id
                WHERE COALESCE(gcm.membercontrol, $na) = $na
          }
      . $dbh->sql_group_by(
      'bugs.product_id, bgm.group_id',
      'gcm.membercontrol, groups.name, products.name'
      )
  );

  foreach (@$invalid_combinations) {
    my ($product_id, $group_id, $currentmembercontrol, $group_name, $product_name)
      = @$_;

    $counter++;
    if (defined($currentmembercontrol)) {
      Status('group_control_map_entries_update',
        {group_name => $group_name, product_name => $product_name});
      $updatesth->execute($group_id, $product_id);
    }
    else {
      Status('group_control_map_entries_generation',
        {group_name => $group_name, product_name => $product_name});
      $insertsth->execute($group_id, $product_id);
    }
  }

  Status('group_control_map_entries_repaired', {counter => $counter});
  $clear_memcached = 1 if $counter;
}

###########################################################################
# Fix missing creation date
###########################################################################

if ($cgi->param('repair_creation_date')) {
  Status('bug_creation_date_start');

  my $bug_ids = $dbh->selectcol_arrayref(
    'SELECT bug_id FROM bugs
                                            WHERE creation_ts IS NULL'
  );

  my $sth_UpdateDate = $dbh->prepare(
    'UPDATE bugs SET creation_ts = ?
                                        WHERE bug_id = ?'
  );

  # All bugs have an entry in the 'longdescs' table when they are created,
  # even if no comment is required.
  my $sth_getDate = $dbh->prepare(
    'SELECT MIN(bug_when) FROM longdescs
                                     WHERE bug_id = ?'
  );

  foreach my $bugid (@$bug_ids) {
    $sth_getDate->execute($bugid);
    my $date = $sth_getDate->fetchrow_array;
    $sth_UpdateDate->execute($date, $bugid);
  }
  Status('bug_creation_date_fixed', {bug_count => scalar(@$bug_ids)});
  $clear_memcached = 1 if @$bug_ids;
}

###########################################################################
# Fix everconfirmed
###########################################################################

if ($cgi->param('repair_everconfirmed')) {
  Status('everconfirmed_start');

  my @confirmed_open_states = grep { $_ ne 'UNCONFIRMED' } BUG_STATE_OPEN;
  my $confirmed_open_states
    = join(', ', map { $dbh->quote($_) } @confirmed_open_states);

  $dbh->do("UPDATE bugs SET everconfirmed = 0 WHERE bug_status = 'UNCONFIRMED'");
  $dbh->do(
    "UPDATE bugs SET everconfirmed = 1 WHERE bug_status IN ($confirmed_open_states)"
  );

  Status('everconfirmed_end');
  $clear_memcached = 1;
}

###########################################################################
# Fix entries in Bugs full_text
###########################################################################

if ($cgi->param('repair_bugs_fulltext')) {
  Status('bugs_fulltext_start');

  my $bug_ids = $dbh->selectcol_arrayref(
    'SELECT bugs.bug_id
                                            FROM bugs
                                            LEFT JOIN bugs_fulltext
                                            ON bugs_fulltext.bug_id = bugs.bug_id
                                            WHERE bugs_fulltext.bug_id IS NULL'
  );

  foreach my $bugid (@$bug_ids) {
    Bugzilla::Bug->new($bugid)->_sync_fulltext(new_bug => 1);
  }

  Status('bugs_fulltext_fixed', {bug_count => scalar(@$bug_ids)});
  $clear_memcached = 1 if @$bug_ids;
}

###########################################################################
# Send unsent mail
###########################################################################

if ($cgi->param('rescanallBugMail')) {
  require Bugzilla::BugMail;

  Status('send_bugmail_start');
  my $time = $dbh->sql_date_math('NOW()', '-', 30, 'MINUTE');

  my $list = $dbh->selectcol_arrayref(
    qq{
                                        SELECT bug_id
                                          FROM bugs 
                                         WHERE (lastdiffed IS NULL
                                                OR lastdiffed < delta_ts)
                                           AND delta_ts < $time
                                      ORDER BY bug_id}
  );

  Status('send_bugmail_status', {bug_count => scalar(@$list)});

  # We cannot simply look at the bugs_activity table to find who did the
  # last change in a given bug, as e.g. adding a comment doesn't add any
  # entry to this table. And some other changes may be private
  # (such as time-related changes or private attachments or comments)
  # and so choosing this user as being the last one having done a change
  # for the bug may be problematic. So the best we can do at this point
  # is to choose the currently logged in user for email notification.
  $vars->{'changer'} = $user;

  foreach my $bugid (@$list) {
    Bugzilla::BugMail::Send($bugid, $vars);
  }

  if (@$list) {
    Status('send_bugmail_end');
    Bugzilla->memcached->clear_all();
  }

  unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
    $template->process('global/footer.html.tmpl', $vars)
      || ThrowTemplateError($template->error());
  }
  exit;
}

###########################################################################
# Remove all references to deleted bugs
###########################################################################

if ($cgi->param('remove_invalid_bug_references')) {
  Status('bug_reference_deletion_start');

  $dbh->bz_start_transaction();

  foreach my $pair (
    'attachments/',           'bug_group_map/',
    'bugs_activity/',         'bugs_fulltext/',
    'cc/',                    'dependencies/blocked',
    'dependencies/dependson', 'duplicates/dupe',
    'duplicates/dupe_of',     'flags/',
    'keywords/',              'longdescs/'
    )
  {

    my ($table, $field) = split('/', $pair);
    $field ||= "bug_id";

    my $bug_ids = $dbh->selectcol_arrayref(
      "SELECT $table.$field FROM $table
                                    LEFT JOIN bugs ON $table.$field = bugs.bug_id
                                    WHERE bugs.bug_id IS NULL"
    );

    if (scalar(@$bug_ids)) {
      $dbh->do("DELETE FROM $table WHERE $field IN (" . join(',', @$bug_ids) . ")");
      $clear_memcached = 1;
    }
  }

  $dbh->bz_commit_transaction();
  Status('bug_reference_deletion_end');
}

###########################################################################
# Remove all references to deleted attachments
###########################################################################

if ($cgi->param('remove_invalid_attach_references')) {
  Status('attachment_reference_deletion_start');

  $dbh->bz_start_transaction();

  my $attach_ids = $dbh->selectcol_arrayref(
    'SELECT attach_data.id
                                    FROM attach_data
                               LEFT JOIN attachments
                                      ON attachments.attach_id = attach_data.id
                                   WHERE attachments.attach_id IS NULL'
  );

  if (scalar(@$attach_ids)) {
    $dbh->do(
      'DELETE FROM attach_data WHERE id IN (' . join(',', @$attach_ids) . ')');
  }

  $dbh->bz_commit_transaction();
  Status('attachment_reference_deletion_end');
  $clear_memcached = 1 if @$attach_ids;
}

###########################################################################
# Remove all references to deleted users or groups from whines
###########################################################################

if ($cgi->param('remove_old_whine_targets')) {
  Status('whines_obsolete_target_deletion_start');

  $dbh->bz_start_transaction();

  foreach my $target (['groups', 'id', MAILTO_GROUP],
    ['profiles', 'userid', MAILTO_USER])
  {
    my ($table, $col, $type) = @$target;
    my $old_ids = $dbh->selectcol_arrayref(
      "SELECT DISTINCT mailto
                                      FROM whine_schedules
                                 LEFT JOIN $table
                                        ON $table.$col = whine_schedules.mailto
                                     WHERE mailto_type = $type AND $table.$col IS NULL"
    );

    if (scalar(@$old_ids)) {
      $dbh->do(
        "DELETE FROM whine_schedules
                       WHERE mailto_type = $type AND mailto IN ("
          . join(',', @$old_ids) . ")"
      );
      $clear_memcached = 1;
    }
  }
  $dbh->bz_commit_transaction();
  Status('whines_obsolete_target_deletion_end');
}

# If any repairs were attempted or made, we need to clear memcached to ensure
# state is consistent.
Bugzilla->memcached->clear_all() if $clear_memcached;

###########################################################################
# Repair hook
###########################################################################

Bugzilla::Hook::process('sanitycheck_repair', {status => \&Status});

###########################################################################
# Checks
###########################################################################
Status('checks_start');

###########################################################################
# Perform referential (cross) checks
###########################################################################

# This checks that a simple foreign key has a valid primary key value.  NULL
# references are acceptable and cause no problem.
#
# The first parameter is the primary key table name.
# The second parameter is the primary key field name.
# Each successive parameter represents a foreign key, it must be a list
# reference, where the list has:
#   the first value is the foreign key table name.
#   the second value is the foreign key field name.
#   the third value is optional and represents a field on the foreign key
#     table to display when the check fails.
#   the fourth value is optional and is a list reference to values that
#     are excluded from checking.
#
# FIXME: The excluded values parameter should go away - the QA contact
#        fields should use NULL instead - see bug #109474.
#        The same goes for series; no bug for that yet.

sub CrossCheck {
  my $table = shift @_;
  my $field = shift @_;
  my $dbh   = Bugzilla->dbh;

  Status('cross_check_to', {table => $table, field => $field});

  while (@_) {
    my $ref = shift @_;
    my ($refertable, $referfield, $keyname, $exceptions) = @$ref;

    $exceptions ||= [];
    my %exceptions = map { $_ => 1 } @$exceptions;

    Status('cross_check_from', {table => $refertable, field => $referfield});

    my $query
      = qq{SELECT DISTINCT $refertable.$referfield}
      . ($keyname ? qq{, $refertable.$keyname } : q{})
      . qq{ FROM $refertable
                    LEFT JOIN $table
                           ON $refertable.$referfield = $table.$field
                        WHERE $table.$field IS NULL
                          AND $refertable.$referfield IS NOT NULL};

    my $sth = $dbh->prepare($query);
    $sth->execute;

    my $has_bad_references = 0;

    while (my ($value, $key) = $sth->fetchrow_array) {
      next if $exceptions{$value};
      Status(
        'cross_check_alert',
        {
          value   => $value,
          table   => $refertable,
          field   => $referfield,
          keyname => $keyname,
          key     => $key
        },
        'alert'
      );
      $has_bad_references = 1;
    }

    # References to non existent bugs can be safely removed, bug 288461
    if ($table eq 'bugs' && $has_bad_references) {
      Status('cross_check_bug_has_references');
    }

    # References to non existent attachments can be safely removed.
    if ($table eq 'attachments' && $has_bad_references) {
      Status('cross_check_attachment_has_references');
    }
  }
}

CrossCheck('classifications', 'id', ['products', 'classification_id']);

CrossCheck("keyworddefs", "id", ["keywords", "keywordid"]);

CrossCheck(
  "fielddefs", "id",
  ["bugs_activity",     "fieldid"],
  ['profiles_activity', 'fieldid']
);

CrossCheck(
  "flagtypes", "id",
  ["flags",          "type_id"],
  ["flagexclusions", "type_id"],
  ["flaginclusions", "type_id"]
);

CrossCheck(
  "bugs",
  "bug_id",
  ["bugs_activity", "bug_id"],
  ["bug_group_map", "bug_id"],
  ["bugs_fulltext", "bug_id"],
  ["attachments",   "bug_id"],
  ["cc",            "bug_id"],
  ["longdescs",     "bug_id"],
  ["dependencies",  "blocked"],
  ["dependencies",  "dependson"],
  ['flags',         'bug_id'],
  ["keywords",      "bug_id"],
  ["duplicates", "dupe_of", "dupe"],
  ["duplicates", "dupe",    "dupe_of"]
);

CrossCheck(
  "groups",
  "id",
  ["bug_group_map",        "group_id"],
  ['category_group_map',   'group_id'],
  ["group_group_map",      "grantor_id"],
  ["group_group_map",      "member_id"],
  ["group_control_map",    "group_id"],
  ["namedquery_group_map", "group_id"],
  ["user_group_map",       "group_id"],
  ["flagtypes",            "grant_group_id"],
  ["flagtypes",            "request_group_id"]
);

CrossCheck(
  "namedqueries", "id",
  ["namedqueries_link_in_footer", "namedquery_id"],
  ["namedquery_group_map",        "namedquery_id"],
);

CrossCheck(
  "profiles",
  "userid",
  ['profiles_activity', 'userid'],
  ['profiles_activity', 'who'],
  ['email_setting',     'user_id'],
  ['profile_setting',   'user_id'],
  ["bugs",                        "reporter",         "bug_id"],
  ["bugs",                        "assigned_to",      "bug_id"],
  ["bugs",                        "qa_contact",       "bug_id"],
  ["attachments",                 "submitter_id",     "bug_id"],
  ['flags',                       'setter_id',        'bug_id'],
  ['flags',                       'requestee_id',     'bug_id'],
  ["bugs_activity",               "who",              "bug_id"],
  ["cc",                          "who",              "bug_id"],
  ['quips',                       'userid'],
  ["longdescs",                   "who",              "bug_id"],
  ["logincookies",                "userid"],
  ["namedqueries",                "userid"],
  ["namedqueries_link_in_footer", "user_id"],
  ['series',                      'creator',          'series_id'],
  ["watch",                       "watcher"],
  ["watch",                       "watched"],
  ['whine_events',                'owner_userid'],
  ["tokens",                      "userid"],
  ["user_group_map",              "user_id"],
  ["components",                  "initialowner",     "name"],
  ["components",                  "initialqacontact", "name"],
  ["component_cc",                "user_id"]
);

CrossCheck(
  "products",
  "id",
  ["bugs",              "product_id", "bug_id"],
  ["components",        "product_id", "name"],
  ["milestones",        "product_id", "value"],
  ["versions",          "product_id", "value"],
  ["group_control_map", "product_id"],
  ["flaginclusions",    "product_id", "type_id"],
  ["flagexclusions",    "product_id", "type_id"]
);

CrossCheck(
  "components", "id",
  ["component_cc", "component_id"],
  ["flagexclusions", "component_id", "type_id"],
  ["flaginclusions", "component_id", "type_id"]
);

# Check the former enum types -mkanat@bugzilla.org
CrossCheck("bug_status", "value", ["bugs", "bug_status", "bug_id"]);

CrossCheck("resolution", "value", ["bugs", "resolution", "bug_id"]);

CrossCheck("bug_severity", "value", ["bugs", "bug_severity", "bug_id"]);

CrossCheck("op_sys", "value", ["bugs", "op_sys", "bug_id"]);

CrossCheck("priority", "value", ["bugs", "priority", "bug_id"]);

CrossCheck("rep_platform", "value", ["bugs", "rep_platform", "bug_id"]);

CrossCheck('series', 'series_id', ['series_data', 'series_id']);

CrossCheck(
  'series_categories', 'id',
  ['series',             'category'],
  ["category_group_map", "category_id"],
  ["series",             "subcategory"]
);

CrossCheck(
  'whine_events', 'id',
  ['whine_queries',   'eventid'],
  ['whine_schedules', 'eventid']
);

CrossCheck(
  'attachments', 'attach_id',
  ['attach_data',   'id'],
  ['bugs_activity', 'attach_id']
);

CrossCheck(
  'bug_status', 'id',
  ['status_workflow', 'old_status'],
  ['status_workflow', 'new_status']
);

###########################################################################
# Perform double field referential (cross) checks
###########################################################################

# This checks that a compound two-field foreign key has a valid primary key
# value.  NULL references are acceptable and cause no problem.
#
# The first parameter is the primary key table name.
# The second parameter is the primary key first field name.
# The third parameter is the primary key second field name.
# Each successive parameter represents a foreign key, it must be a list
# reference, where the list has:
#   the first value is the foreign key table name
#   the second value is the foreign key first field name.
#   the third value is the foreign key second field name.
#   the fourth value is optional and represents a field on the foreign key
#     table to display when the check fails

sub DoubleCrossCheck {
  my $table  = shift @_;
  my $field1 = shift @_;
  my $field2 = shift @_;
  my $dbh    = Bugzilla->dbh;

  Status('double_cross_check_to',
    {table => $table, field1 => $field1, field2 => $field2});

  while (@_) {
    my $ref = shift @_;
    my ($refertable, $referfield1, $referfield2, $keyname) = @$ref;

    Status('double_cross_check_from',
      {table => $refertable, field1 => $referfield1, field2 => $referfield2});

    my $d_cross_check = $dbh->selectall_arrayref(
      qq{
                        SELECT DISTINCT $refertable.$referfield1, 
                                        $refertable.$referfield2 }
        . ($keyname ? qq{, $refertable.$keyname } : q{}) . qq{ FROM $refertable
                     LEFT JOIN $table
                            ON $refertable.$referfield1 = $table.$field1
                           AND $refertable.$referfield2 = $table.$field2 
                         WHERE $table.$field1 IS NULL 
                           AND $table.$field2 IS NULL 
                           AND $refertable.$referfield1 IS NOT NULL 
                           AND $refertable.$referfield2 IS NOT NULL}
    );

    foreach my $check (@$d_cross_check) {
      my ($value1, $value2, $key) = @$check;
      Status(
        'double_cross_check_alert',
        {
          value1  => $value1,
          value2  => $value2,
          table   => $refertable,
          field1  => $referfield1,
          field2  => $referfield2,
          keyname => $keyname,
          key     => $key
        },
        'alert'
      );
    }
  }
}

DoubleCrossCheck(
  'attachments', 'bug_id', 'attach_id',
  ['flags',         'bug_id', 'attach_id'],
  ['bugs_activity', 'bug_id', 'attach_id']
);

DoubleCrossCheck(
  "components",
  "product_id",
  "id",
  ["bugs",           "product_id", "component_id", "bug_id"],
  ['flagexclusions', 'product_id', 'component_id'],
  ['flaginclusions', 'product_id', 'component_id']
);

DoubleCrossCheck("versions", "product_id", "value",
  ["bugs", "product_id", "version", "bug_id"]);

DoubleCrossCheck(
  "milestones", "product_id", "value",
  ["bugs",     "product_id", "target_milestone", "bug_id"],
  ["products", "id",         "defaultmilestone", "name"]
);

###########################################################################
# Perform login checks
###########################################################################

Status('profile_login_start');

my $sth = $dbh->prepare(q{SELECT userid, email FROM profiles});
$sth->execute;

while (my ($id, $email) = $sth->fetchrow_array) {
  validate_email_syntax($email)
    || Status('profile_login_alert', {id => $id, email => $email}, 'alert');
}

###########################################################################
# Perform keyword checks
###########################################################################

sub check_keywords {
  my $dbh = Bugzilla->dbh;
  my $cgi = Bugzilla->cgi;

  Status('keyword_check_start');

  my %keywordids;
  my $keywords = $dbh->selectall_arrayref(
    q{SELECT id, name
                                                FROM keyworddefs}
  );

  foreach (@$keywords) {
    my ($id, $name) = @$_;
    if ($keywordids{$id}) {
      Status('keyword_check_alert', {id => $id}, 'alert');
    }
    $keywordids{$id} = 1;
    if ($name =~ /[\s,]/) {
      Status('keyword_check_invalid_name', {id => $id}, 'alert');
    }
  }

  my $sth = $dbh->prepare(
    q{SELECT bug_id, keywordid
                                FROM keywords
                            ORDER BY bug_id, keywordid}
  );
  $sth->execute;
  my $lastid;
  my $lastk;
  while (my ($id, $k) = $sth->fetchrow_array) {
    if (!$keywordids{$k}) {
      Status('keyword_check_invalid_id', {id => $k}, 'alert');
    }
    if (defined $lastid && $id eq $lastid && $k eq $lastk) {
      Status('keyword_check_duplicated_ids', {id => $id}, 'alert');
    }
    $lastid = $id;
    $lastk  = $k;
  }
}

###########################################################################
# Check for flags being in incorrect products and components
###########################################################################

Status('flag_check_start');

my $invalid_flags = $dbh->selectall_arrayref(
  'SELECT DISTINCT flags.id, flags.bug_id, flags.attach_id
          FROM flags
    INNER JOIN bugs
            ON flags.bug_id = bugs.bug_id
     LEFT JOIN flaginclusions AS i
            ON flags.type_id = i.type_id
           AND (bugs.product_id = i.product_id OR i.product_id IS NULL)
           AND (bugs.component_id = i.component_id OR i.component_id IS NULL)
         WHERE i.type_id IS NULL'
);

my @invalid_flags = @$invalid_flags;

$invalid_flags = $dbh->selectall_arrayref(
  'SELECT DISTINCT flags.id, flags.bug_id, flags.attach_id
          FROM flags
    INNER JOIN bugs
            ON flags.bug_id = bugs.bug_id
    INNER JOIN flagexclusions AS e
            ON flags.type_id = e.type_id
         WHERE (bugs.product_id = e.product_id OR e.product_id IS NULL)
           AND (bugs.component_id = e.component_id OR e.component_id IS NULL)'
);

push(@invalid_flags, @$invalid_flags);

if (scalar(@invalid_flags)) {
  if ($cgi->param('remove_invalid_flags')) {
    Status('flag_deletion_start');
    my @flag_ids = map { $_->[0] } @invalid_flags;

    # Silently delete these flags, with no notification to requesters/setters.
    $dbh->do('DELETE FROM flags WHERE id IN (' . join(',', @flag_ids) . ')');
    Status('flag_deletion_end');
    Bugzilla->memcached->clear_all();
  }
  else {
    foreach my $flag (@$invalid_flags) {
      my ($flag_id, $bug_id, $attach_id) = @$flag;
      Status('flag_alert',
        {flag_id => $flag_id, attach_id => $attach_id, bug_id => $bug_id}, 'alert');
    }
    Status('flag_fix');
  }
}

###########################################################################
# Check for products with no component
###########################################################################

Status('product_check_start');

my $products_missing_data = $dbh->selectcol_arrayref(
  'SELECT DISTINCT products.name
         FROM products
    LEFT JOIN components
           ON components.product_id = products.id
    LEFT JOIN versions
           ON versions.product_id = products.id
        WHERE components.id IS NULL
           OR versions.id IS NULL'
);

if (scalar(@$products_missing_data)) {
  Status('product_alert', {name => $_}, 'alert') foreach @$products_missing_data;
}

###########################################################################
# General bug checks
###########################################################################

sub BugCheck {
  my ($middlesql, $errortext, $repairparam, $repairtext) = @_;
  my $dbh = Bugzilla->dbh;

  my $badbugs = $dbh->selectcol_arrayref(
    qq{SELECT DISTINCT bugs.bug_id
                                                FROM $middlesql 
                                            ORDER BY bugs.bug_id}
  );

  if (scalar(@$badbugs)) {
    Status('bug_check_alert',
      {errortext => get_string($errortext), badbugs => $badbugs}, 'alert');

    if ($repairparam) {
      $repairtext ||= 'repair_bugs';
      Status('bug_check_repair',
        {param => $repairparam, text => get_string($repairtext)});
    }
  }
}

Status('bug_check_creation_date');

BugCheck(
  "bugs WHERE creation_ts IS NULL", 'bug_check_creation_date_error_text',
  'repair_creation_date',           'bug_check_creation_date_repair_text'
);

Status('bug_check_bugs_fulltext');

BugCheck(
  "bugs LEFT JOIN bugs_fulltext ON bugs_fulltext.bug_id = bugs.bug_id "
    . "WHERE bugs_fulltext.bug_id IS NULL",
  'bug_check_bugs_fulltext_error_text',
  'repair_bugs_fulltext',
  'bug_check_bugs_fulltext_repair_text'
);

Status('bug_check_res_dupl');

BugCheck(
  "bugs INNER JOIN duplicates ON bugs.bug_id = duplicates.dupe "
    . "WHERE bugs.resolution != 'DUPLICATE'",
  'bug_check_res_dupl_error_text'
);

BugCheck(
  "bugs LEFT JOIN duplicates ON bugs.bug_id = duplicates.dupe WHERE "
    . "bugs.resolution = 'DUPLICATE' AND "
    . "duplicates.dupe IS NULL",
  'bug_check_res_dupl_error_text2'
);

Status('bug_check_status_res');

my @open_states = map($dbh->quote($_), BUG_STATE_OPEN);
my $open_states = join(', ', @open_states);

BugCheck("bugs WHERE bug_status IN ($open_states) AND resolution != ''",
  'bug_check_status_res_error_text');
BugCheck("bugs WHERE bug_status NOT IN ($open_states) AND resolution = ''",
  'bug_check_status_res_error_text2');

Status('bug_check_status_everconfirmed');

BugCheck(
  "bugs WHERE bug_status = 'UNCONFIRMED' AND everconfirmed = 1",
  'bug_check_status_everconfirmed_error_text',
  'repair_everconfirmed'
);

my @confirmed_open_states = grep { $_ ne 'UNCONFIRMED' } BUG_STATE_OPEN;
my $confirmed_open_states
  = join(', ', map { $dbh->quote($_) } @confirmed_open_states);

BugCheck(
  "bugs WHERE bug_status IN ($confirmed_open_states) AND everconfirmed = 0",
  'bug_check_status_everconfirmed_error_text2',
  'repair_everconfirmed'
);

###########################################################################
# Control Values
###########################################################################

# Checks for values that are invalid OR
# not among the 9 valid combinations
Status('bug_check_control_values');
my $groups = join(", ",
  (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT, CONTROLMAPMANDATORY));
my $query = qq{
     SELECT COUNT(product_id) 
       FROM group_control_map 
      WHERE membercontrol NOT IN( $groups )
         OR othercontrol NOT IN( $groups )
         OR ((membercontrol != othercontrol)
             AND (membercontrol != } . CONTROLMAPSHOWN . q{)
             AND ((membercontrol != } . CONTROLMAPDEFAULT . q{)
                  OR (othercontrol = } . CONTROLMAPSHOWN . q{)))};

my $entries = $dbh->selectrow_array($query);
Status('bug_check_control_values_alert', {entries => $entries}, 'alert')
  if $entries;

Status('bug_check_control_values_violation');
BugCheck(
  "bugs
         INNER JOIN bug_group_map
            ON bugs.bug_id = bug_group_map.bug_id
          LEFT JOIN group_control_map
            ON bugs.product_id = group_control_map.product_id
           AND bug_group_map.group_id = group_control_map.group_id
         WHERE ((group_control_map.membercontrol = " . CONTROLMAPNA . ")
         OR (group_control_map.membercontrol IS NULL))",
  'bug_check_control_values_error_text', 'createmissinggroupcontrolmapentries',
  'bug_check_control_values_repair_text'
);

BugCheck(
  "bugs
         INNER JOIN group_control_map
            ON bugs.product_id = group_control_map.product_id
         INNER JOIN groups
            ON group_control_map.group_id = groups.id
          LEFT JOIN bug_group_map
            ON bugs.bug_id = bug_group_map.bug_id
           AND group_control_map.group_id = bug_group_map.group_id
         WHERE group_control_map.membercontrol = " . CONTROLMAPMANDATORY . "
           AND bug_group_map.group_id IS NULL
           AND groups.isactive != 0", 'bug_check_control_values_error_text2'
);

###########################################################################
# Unsent mail
###########################################################################

Status('unsent_bugmail_check');

my $time = $dbh->sql_date_math('NOW()', '-', 30, 'MINUTE');
my $badbugs = $dbh->selectcol_arrayref(
  qq{
                    SELECT bug_id 
                      FROM bugs 
                     WHERE (lastdiffed IS NULL OR lastdiffed < delta_ts)
                       AND delta_ts < $time
                  ORDER BY bug_id}
);


if (scalar(@$badbugs > 0)) {
  Status('unsent_bugmail_alert', {badbugs => $badbugs}, 'alert');
  Status('unsent_bugmail_fix');
}

###########################################################################
# Whines
###########################################################################

Status('whines_obsolete_target_start');

my $display_repair_whines_link = 0;
foreach my $target (['groups', 'id', MAILTO_GROUP],
  ['profiles', 'userid', MAILTO_USER])
{
  my ($table, $col, $type) = @$target;
  my $old = $dbh->selectall_arrayref(
    "SELECT whine_schedules.id, mailto
                                          FROM whine_schedules
                                     LEFT JOIN $table
                                            ON $table.$col = whine_schedules.mailto
                                         WHERE mailto_type = $type AND $table.$col IS NULL"
  );

  if (scalar(@$old)) {
    Status('whines_obsolete_target_alert',
      {schedules => $old, type => $type}, 'alert');
    $display_repair_whines_link = 1;
  }
}
Status('whines_obsolete_target_fix') if $display_repair_whines_link;

###########################################################################
# Check hook
###########################################################################

Bugzilla::Hook::process('sanitycheck_check', {status => \&Status});

###########################################################################
# End
###########################################################################

Status('checks_completed');

unless (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) {
  $template->process('global/footer.html.tmpl', $vars)
    || ThrowTemplateError($template->error());
}
